﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using Yorrick.FlickrMetadataSynchr.Data;

namespace Yorrick.FlickrMetadataSynchr.Sync
{
    /// <summary>
    /// Merges metadata
    /// </summary>
    internal class MetadataMerger
    {
        #region Private fields
        /// <summary>
        /// Metadata for the local picture.
        /// </summary>
        private LocalPictureMetadata _lm;

        /// <summary>
        /// Metadata for the Flickr picture.
        /// </summary>
        private FlickrPictureMetadata _fm;

        /// <summary>
        /// Metadata sync strategy settings.
        /// </summary>
        private SyncStrategySettings _strategySettings;

        /// <summary>
        /// Whether or not the local metadata was updated more recently.
        /// </summary>
        private bool _localIsMoreRecent;

        /// <summary>
        /// The string comparer to use for tags.
        /// </summary>
        private StringComparer _tagComparer;
        #endregion

        #region Constructor
        /// <summary>
        /// Constructs an instance to merge metadata between a local and Flickr picture pair using
        /// the specified sync strategy.
        /// </summary>
        /// <param name="lm">The local picture metadata.</param>
        /// <param name="fm">The Flickr picture metadata.</param>
        /// <param name="strategySettings">The sync strategy settings.</param>
        public MetadataMerger(LocalPictureMetadata lm, FlickrPictureMetadata fm, SyncStrategySettings strategySettings)
        {
            this._lm = lm;
            this._fm = fm;
            _strategySettings = strategySettings;

            _localIsMoreRecent = this._lm.LastUpdated >= this._fm.LastUpdated;

            if (_strategySettings.TagsCaseBehavior == TagsCaseBehavior.CaseSensitive)
            {
                _tagComparer = StringComparer.CurrentCulture;
            }
            else
            {
                _tagComparer = StringComparer.CurrentCultureIgnoreCase;
            }
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Merges all metadata using the specified sync strategy.
        /// </summary>
        public void Merge()
        {
            MergeFlickrId();

            MergeTitle();

            MergeDescription();

            MergeGpsCoordinates();

            MergeDateTaken();

            MergeAuthor();

            MergeTags();
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Merges the Flickr id. The Flickr side always wins.
        /// </summary>
        private void MergeFlickrId()
        {
            if (_lm.FlickrId != _fm.FlickrId)
            {
                _lm.FlickrId = _fm.FlickrId;
            }
        }

        /// <summary>
        /// Merges the title.
        /// </summary>
        private void MergeTitle()
        {
            if (_lm.Title != _fm.Title)
            {
                if ((String.IsNullOrEmpty(_lm.Title) || !_localIsMoreRecent) && !String.IsNullOrEmpty(_fm.Title))
                {
                    _lm.Title = _fm.Title;
                }
                else if ((String.IsNullOrEmpty(_fm.Title) || _localIsMoreRecent) && !String.IsNullOrEmpty(_lm.Title))
                {
                    _fm.Title = _lm.Title;
                }
            }
        }

        /// <summary>
        /// Merges the description.
        /// </summary>
        private void MergeDescription()
        {
            if (_lm.Description != _fm.Description)
            {
                if ((String.IsNullOrEmpty(_lm.Description) || !_localIsMoreRecent) && !String.IsNullOrEmpty(_fm.Description))
                {
                    _lm.Description = _fm.Description;
                }
                else if ((String.IsNullOrEmpty(_fm.Description) || _localIsMoreRecent) && !String.IsNullOrEmpty(_lm.Description))
                {
                    _fm.Description = _lm.Description;
                }
            }
        }

        /// <summary>
        /// Merges the GPS geocoordinates.
        /// </summary>
        private void MergeGpsCoordinates()
        {
            if (_lm.GpsLatitude != _fm.GpsLatitude || _lm.GpsLongitude != _fm.GpsLongitude)
            {
                if (((_lm.GpsLatitude == null && _lm.GpsLongitude == null) || !_localIsMoreRecent) &&
                    (_fm.GpsLatitude != null && _fm.GpsLongitude != null))
                {
                    _lm.GpsLatitude = _fm.GpsLatitude;
                    _lm.GpsLongitude = _fm.GpsLongitude;
                }
                else if (((_fm.GpsLatitude == null && _fm.GpsLongitude == null) || _localIsMoreRecent) &&
                         (_lm.GpsLatitude != null && _lm.GpsLongitude != null))
                {
                    _fm.GpsLatitude = _lm.GpsLatitude;
                    _fm.GpsLongitude = _lm.GpsLongitude;
                }
            }
        }

        /// <summary>
        /// Merges the date taken.
        /// </summary>
        private void MergeDateTaken()
        {
            if (_lm.DateTaken != _fm.DateTaken)
            {
                if (!_localIsMoreRecent)
                {
                    _lm.DateTaken = _fm.DateTaken;
                }
                else
                {
                    _fm.DateTaken = _lm.DateTaken;
                }
            }
        }

        /// <summary>
        /// Merges author information.
        /// </summary>
        public void MergeAuthor()
        {
            if (_strategySettings.AuthorSyncBehavior == AuthorSyncBehavior.DoNothing)
            {
                return;
            }

            if (_lm.Author != _fm.Author)
            {
                if ((String.IsNullOrEmpty(_lm.Author) || !_localIsMoreRecent) && !String.IsNullOrEmpty(_fm.Author))
                {
                    _lm.Author = _fm.Author;
                }
                else if ((String.IsNullOrEmpty(_fm.Author) || _localIsMoreRecent) && !String.IsNullOrEmpty(_lm.Author))
                {
                    _fm.Author = _lm.Author;
                }
            }

            // Restore the local author to the original value, if it already had an author.
            // On Flickr the "author" is really just the owner, and we don't want to overwrite
            // the real author with the owner.
            if (!String.IsNullOrEmpty(_lm.AuthorOriginal)
               && _lm.AuthorChanged)
            {
                _lm.Author = _lm.AuthorOriginal;

                Trace.TraceInformation("Resetting author to original value on local picture '{0}'", _lm.FilePath);
            }
        }

        /// <summary>
        /// Merges the tags for a picture pair, so that there are no duplicates. Does
        /// nothing if tags should not be merged. It can also add a Flickr id as a special machine tag.
        /// </summary>
        private void MergeTags()
        {
            if (_strategySettings.TagsSyncBehavior == TagsSyncBehavior.None)
            {
                return;
            }

            if (PictureMetadata.AreTagsEqual(_lm, _fm, _tagComparer))
            {
                return;
            }

            if (_strategySettings.TagsSyncBehavior == TagsSyncBehavior.NewestWins)
            {
                if (_localIsMoreRecent)
                {
                    _fm.Tags = _lm.Tags;
                }
                else
                {
                    _lm.Tags = _fm.Tags;
                }
            }
            else if (_strategySettings.TagsSyncBehavior == TagsSyncBehavior.Merge)
            {
                string[] mergedTags = GetMergedTags(_lm.Tags, _fm.Tags, _localIsMoreRecent);

                _fm.Tags = mergedTags;
                _lm.Tags = mergedTags;
            }
        }

        /// <summary>
        /// Merges two arrays of tags, so that there are no duplicates.
        /// </summary>
        /// <param name="tags1">The first array of tags.</param>
        /// <param name="tags2">The second array of tags.</param>
        /// <param name="firstIsMoreRecent"><c>True</c> if the first array of tags is more recent, <c>false</c> otherwise.</param>
        /// <returns>The merged array of tags.</returns>
        private string[] GetMergedTags(string[] tags1, string[] tags2, bool firstIsMoreRecent)
        {
            Dictionary<string, string> uniqueTags = GetUniqueTagsDictionary(tags1, tags2, firstIsMoreRecent);

            List<string> mergedTagList = new List<string>(uniqueTags.Count);

            if (_strategySettings.TagsSortBehavior == TagsSortBehavior.SortAlphabetically)
            {
                mergedTagList.InsertRange(0, uniqueTags.Values);
                mergedTagList.Sort(_tagComparer);
            }
            else if (_strategySettings.TagsSortBehavior == TagsSortBehavior.OriginalOrder)
            {
                foreach (string tag1 in tags1)
                {
                    mergedTagList.Add(uniqueTags[tag1]);
                    uniqueTags.Remove(tag1);
                }

                foreach (string tag2 in tags2)
                {
                    if (uniqueTags.ContainsKey(tag2))
                    {
                        mergedTagList.Add(tag2);
                    }
                }
            }
            else
            {
                throw new InvalidOperationException("Unsupported TagsSortBehavior " + _strategySettings.TagsSortBehavior.ToString() + ".");
            }

            return mergedTagList.ToArray();
        }

        /// <summary>
        /// Merges two arrays of tags, so that there are no duplicates.
        /// </summary>
        /// <param name="tags1">The first array of tags.</param>
        /// <param name="tags2">The second array of tags.</param>
        /// <param name="firstIsMoreRecent"><c>True</c> if the first array of tags is more recent, <c>false</c> otherwise.</param>
        /// <returns>The merged array of tags.</returns>
        private Dictionary<string, string> GetUniqueTagsDictionary(string[] tags1, string[] tags2, bool firstIsMoreRecent)
        {
            Dictionary<string, string> mergedTags = new Dictionary<string, string>(tags1.Length + tags2.Length, _tagComparer);

            foreach (string tag in tags1)
            {
                mergedTags.Add(tag, tag);
            }

            bool mostRecentUpdatePreferred = (_strategySettings.TagsCaseBehavior == TagsCaseBehavior.MostRecentUpdatedPreferred);
            bool lowerCasePreferred = (_strategySettings.TagsCaseBehavior == TagsCaseBehavior.LowercaseFirstLetterPreferredForDuplicates);
            bool upperCasePreferred = (_strategySettings.TagsCaseBehavior == TagsCaseBehavior.UppercaseFirstLetterPreferredForDuplicates);

            foreach (string tag2 in tags2)
            {
                if (!mergedTags.ContainsKey(tag2))
                {
                    mergedTags.Add(tag2, tag2);
                }
                else
                {
                    // Duplicate tag, let the settings decide what should happen.
                    if (mostRecentUpdatePreferred)
                    {
                        if (!firstIsMoreRecent)
                        {
                            mergedTags[tag2] = tag2;
                        }
                    }
                    else if (lowerCasePreferred)
                    {
                        if (Char.IsLower(tag2[0]))
                        {
                            mergedTags[tag2] = tag2;
                        }
                    }
                    else if (upperCasePreferred)
                    {
                        if (Char.IsUpper(tag2[0]))
                        {
                            mergedTags[tag2] = tag2;
                        }
                    }
                }
            }
            return mergedTags;
        }
        #endregion
    }
}
